package tld.test.interpreter.runtime
{
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.utils.Dictionary;
	import flash.utils.describeType;
	
	import tld.test.interpreter.Context;
	import tld.test.interpreter.Evaluator;
	import tld.test.interpreter.Expression;
	import tld.test.interpreter.IContext;
	import tld.test.interpreter.Parser;
	import tld.test.interpreter.Printer;
	import tld.test.interpreter.language.LanguageContext;
	import tld.test.interpreter.logic.LogicContext;
	import tld.test.interpreter.math.MathContext;
	import tld.test.interpreter.symbols.SymbolsContext;

	/**
	 * This runtime will execute a very simple Lisp-like
	 * language developed for the purpose of this demo
	 * @author wvxvw
	 */
	public class Runtime implements IContext
	{
		private const _loader:URLLoader = new URLLoader();
		private const _names:Object =
		{
			"print" : print,
			"eval" : eval,
			"load" : load,
			"read" : read,
			"run" : run
		}
		private var _context:IContext;
		private var _evaluator:Evaluator;
		private var _printer:Printer;
		private var _pareser:Parser;
		private var _last:Object;
		private var _printerFunction:Function;
		
		public function Runtime(printer:Function = null)
		{
			super();
			this.init();
		}
		
		public function setPrinter(printer:Function):void
		{
			this._printerFunction = printer;
		}
		
		private function init():void
		{
			this._evaluator = new Evaluator();
			this._context =
				new LanguageContext(
					new SymbolsContext(new MathContext(
						new LogicContext(this))));
			this._printer = new Printer(trace);
			this._pareser = new Parser();
		}
		
		public function addContext(context:IContext):void
		{
			trace("addContext", context);
			this.setParent(context);
			this.traceScopesChain();
		}
		
		public function removeContext(context:IContext):void
		{
			var current:IContext = this._context;
			var last:IContext = this._context;
			
			while (current)
			{
				if (context == current)
				{
					this._context = last.setParent(
						context.getParent());
					trace("removed context", current);
					trace("replaced with", this._context);
				}
				last = current;
				current = current.getParent();
			}
			this.traceScopesChain();
		}
		
		public function load(url:String):void
		{
			this._loader.addEventListener(
				Event.COMPLETE, this.completeHandler);
			this._loader.addEventListener(
				IOErrorEvent.IO_ERROR, this.errorHandler);
			this._loader.addEventListener(
				SecurityErrorEvent.SECURITY_ERROR, this.errorHandler);
			this._loader.load(new URLRequest(url));
		}
		
		public function last():Object { return this._last; }
		
		public function run(text:String):String
		{
			do
			{
				trace("runtime evaling sexp", this._pareser.tail(), "|");
				this._last = this._evaluator.eval(this._pareser, 
					this._pareser.readExpression(text), 
					this._context);
				text = this._pareser.tail();
			}
			while (this._pareser.canRead(text));
			return this._pareser.tail();
		}
		
		public function canRead(text:String):Boolean
		{
			return this._pareser.canRead(text);
		}
		
		public function eval(expression:Expression):Object
		{
			return this._evaluator.eval(
				this._pareser, expression, this._context);
		}
		
		public function read(text:String):Expression
		{
			return this._pareser.readExpression(text);
		}
		
		public function print(...args):void
		{
			var worker:Function = 
				this._printerFunction || this._printer.print;
			worker.apply(null, args);
		}
		
		public function getParent():IContext { return null; }
		
		public function setParent(parent:IContext):IContext
		{
			if (this._context) parent.setParent(this._context);
			return this._context = parent;
		}
		
		public function getNames():Object
		{
			return this._names;
		}
		
		public function put(name:String, value:Object = null):void
		{
			this[name] = value;
		}
		
		public function resolve(name:String):Object
		{
			return this[name];
		}
		
		private function errorHandler(event:ErrorEvent):void
		{
			this._printer.print(event);
		}
		
		private function completeHandler(event:Event):void
		{
			this.run(String(this._loader.data));
		}
		
		private function traceScopesChain():void
		{
			var current:IContext = this._context;
			var last:IContext;
			var lookup:Dictionary = new Dictionary();
			
			while (current)
			{
				if (current in lookup)
				{
					last.setParent(null);
					break;
				}
				last = current;
				current = current.getParent();
				lookup[last] = true;
			}
		}
	}
}